/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack.packet;

import java.util.*;

/**
 * Represents a XMPP error sub-packet. Typically, a server responds to a request
 * that has problems by sending the packet back and including an error packet.
 * Each error has a code, type, error condition as well as as an optional text
 * explanation. Typical errors are:
 * <p>
 * 
 * <table border=1>
 * <hr>
 * <td><b>Code</b></td>
 * <td><b>XMPP Error</b></td>
 * <td><b>Type</b></td>
 * </hr>
 * <tr>
 * <td>500</td>
 * <td>interna-server-error</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>403</td>
 * <td>forbidden</td>
 * <td>AUTH</td>
 * </tr>
 * <tr>
 * <td>400</td
 * <td>bad-request</td>
 * <td>MODIFY</td>>
 * </tr>
 * <tr>
 * <td>404</td>
 * <td>item-not-found</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>409</td>
 * <td>conflict</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>501</td>
 * <td>feature-not-implemented</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>302</td>
 * <td>gone</td>
 * <td>MODIFY</td>
 * </tr>
 * <tr>
 * <td>400</td>
 * <td>jid-malformed</td>
 * <td>MODIFY</td>
 * </tr>
 * <tr>
 * <td>406</td>
 * <td>no-acceptable</td>
 * <td>MODIFY</td>
 * </tr>
 * <tr>
 * <td>405</td>
 * <td>not-allowed</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>401</td>
 * <td>not-authorized</td>
 * <td>AUTH</td>
 * </tr>
 * <tr>
 * <td>402</td>
 * <td>payment-required</td>
 * <td>AUTH</td>
 * </tr>
 * <tr>
 * <td>404</td>
 * <td>recipient-unavailable</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>302</td>
 * <td>redirect</td>
 * <td>MODIFY</td>
 * </tr>
 * <tr>
 * <td>407</td>
 * <td>registration-required</td>
 * <td>AUTH</td>
 * </tr>
 * <tr>
 * <td>404</td>
 * <td>remote-server-not-found</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>504</td>
 * <td>remote-server-timeout</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>502</td>
 * <td>remote-server-error</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>500</td>
 * <td>resource-constraint</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>503</td>
 * <td>service-unavailable</td>
 * <td>CANCEL</td>
 * </tr>
 * <tr>
 * <td>407</td>
 * <td>subscription-required</td>
 * <td>AUTH</td>
 * </tr>
 * <tr>
 * <td>500</td>
 * <td>undefined-condition</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>400</td>
 * <td>unexpected-condition</td>
 * <td>WAIT</td>
 * </tr>
 * <tr>
 * <td>408</td>
 * <td>request-timeout</td>
 * <td>CANCEL</td>
 * </tr>
 * </table>
 * 
 * @author Matt Tucker
 */
public class XMPPError {

	private int code;
	private Type type;
	private String condition;
	private String message;
	private List<PacketExtension> applicationExtensions = null;

	/**
	 * Creates a new error with the specified condition infering the type and
	 * code. If the Condition is predefined, client code should be like: new
	 * XMPPError(XMPPError.Condition.remote_server_timeout); If the Condition is
	 * not predefined, invocations should be like new XMPPError(new
	 * XMPPError.Condition("my_own_error"));
	 * 
	 * @param condition
	 *            the error condition.
	 */
	public XMPPError(Condition condition) {
		this.init(condition);
		this.message = null;
	}

	/**
	 * Creates a new error with the specified condition and message infering the
	 * type and code. If the Condition is predefined, client code should be
	 * like: new XMPPError(XMPPError.Condition.remote_server_timeout,
	 * "Error Explanation"); If the Condition is not predefined, invocations
	 * should be like new XMPPError(new XMPPError.Condition("my_own_error"),
	 * "Error Explanation");
	 * 
	 * @param condition
	 *            the error condition.
	 * @param messageText
	 *            a message describing the error.
	 */
	public XMPPError(Condition condition, String messageText) {
		this.init(condition);
		this.message = messageText;
	}

	/**
	 * Creates a new error with the specified code and no message.
	 * 
	 * @param code
	 *            the error code.
	 * @deprecated new errors should be created using the constructor
	 *             XMPPError(condition)
	 */
	public XMPPError(int code) {
		this.code = code;
		this.message = null;
	}

	/**
	 * Creates a new error with the specified code and message. deprecated
	 * 
	 * @param code
	 *            the error code.
	 * @param message
	 *            a message describing the error.
	 * @deprecated new errors should be created using the constructor
	 *             XMPPError(condition, message)
	 */
	public XMPPError(int code, String message) {
		this.code = code;
		this.message = message;
	}

	/**
	 * Creates a new error with the specified code, type, condition and message.
	 * This constructor is used when the condition is not recognized
	 * automatically by XMPPError i.e. there is not a defined instance of
	 * ErrorCondition or it does not applies the default specification.
	 * 
	 * @param code
	 *            the error code.
	 * @param type
	 *            the error type.
	 * @param condition
	 *            the error condition.
	 * @param message
	 *            a message describing the error.
	 * @param extension
	 *            list of packet extensions
	 */
	public XMPPError(int code, Type type, String condition, String message,
			List<PacketExtension> extension) {
		this.code = code;
		this.type = type;
		this.condition = condition;
		this.message = message;
		this.applicationExtensions = extension;
	}

	/**
	 * Initialize the error infering the type and code for the received
	 * condition.
	 * 
	 * @param condition
	 *            the error condition.
	 */
	private void init(Condition condition) {
		// Look for the condition and its default code and type
		ErrorSpecification defaultErrorSpecification = ErrorSpecification
				.specFor(condition);
		this.condition = condition.value;
		if (defaultErrorSpecification != null) {
			// If there is a default error specification for the received
			// condition,
			// it get configured with the infered type and code.
			this.type = defaultErrorSpecification.getType();
			this.code = defaultErrorSpecification.getCode();
		}
	}

	/**
	 * Returns the error condition.
	 * 
	 * @return the error condition.
	 */
	public String getCondition() {
		return condition;
	}

	/**
	 * Returns the error type.
	 * 
	 * @return the error type.
	 */
	public Type getType() {
		return type;
	}

	/**
	 * Returns the error code.
	 * 
	 * @return the error code.
	 */
	public int getCode() {
		return code;
	}

	/**
	 * Returns the message describing the error, or null if there is no message.
	 * 
	 * @return the message describing the error, or null if there is no message.
	 */
	public String getMessage() {
		return message;
	}

	/**
	 * Returns the error as XML.
	 * 
	 * @return the error as XML.
	 */
	public String toXML() {
		StringBuilder buf = new StringBuilder();
		buf.append("<error code=\"").append(code).append("\"");
		if (type != null) {
			buf.append(" type=\"");
			buf.append(type.name());
			buf.append("\"");
		}
		buf.append(">");
		if (condition != null) {
			buf.append("<").append(condition);
			buf.append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>");
		}
		if (message != null) {
			buf.append("<text xml:lang=\"en\" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\">");
			buf.append(message);
			buf.append("</text>");
		}
		for (PacketExtension element : this.getExtensions()) {
			buf.append(element.toXML());
		}
		buf.append("</error>");
		return buf.toString();
	}

	public String toString() {
		StringBuilder txt = new StringBuilder();
		if (condition != null) {
			txt.append(condition);
		}
		txt.append("(").append(code).append(")");
		if (message != null) {
			txt.append(" ").append(message);
		}
		return txt.toString();
	}

	/**
	 * Returns an Iterator for the error extensions attached to the xmppError.
	 * An application MAY provide application-specific error information by
	 * including a properly-namespaced child in the error element.
	 * 
	 * @return an Iterator for the error extensions.
	 */
	public synchronized List<PacketExtension> getExtensions() {
		if (applicationExtensions == null) {
			return Collections.emptyList();
		}
		return Collections.unmodifiableList(applicationExtensions);
	}

	/**
	 * Returns the first patcket extension that matches the specified element
	 * name and namespace, or <tt>null</tt> if it doesn't exist.
	 * 
	 * @param elementName
	 *            the XML element name of the packet extension.
	 * @param namespace
	 *            the XML element namespace of the packet extension.
	 * @return the extension, or <tt>null</tt> if it doesn't exist.
	 */
	public synchronized PacketExtension getExtension(String elementName,
			String namespace) {
		if (applicationExtensions == null || elementName == null
				|| namespace == null) {
			return null;
		}
		for (PacketExtension ext : applicationExtensions) {
			if (elementName.equals(ext.getElementName())
					&& namespace.equals(ext.getNamespace())) {
				return ext;
			}
		}
		return null;
	}

	/**
	 * Adds a packet extension to the error.
	 * 
	 * @param extension
	 *            a packet extension.
	 */
	public synchronized void addExtension(PacketExtension extension) {
		if (applicationExtensions == null) {
			applicationExtensions = new ArrayList<PacketExtension>();
		}
		applicationExtensions.add(extension);
	}

	/**
	 * Set the packet extension to the error.
	 * 
	 * @param extension
	 *            a packet extension.
	 */
	public synchronized void setExtension(List<PacketExtension> extension) {
		applicationExtensions = extension;
	}

	/**
	 * A class to represent the type of the Error. The types are:
	 * 
	 * <ul>
	 * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary)
	 * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable)
	 * <li>XMPPError.Type.MODIFY - retry after changing the data sent
	 * <li>XMPPError.Type.AUTH - retry after providing credentials
	 * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning)
	 * </ul>
	 */
	public static enum Type {
		WAIT, CANCEL, MODIFY, AUTH, CONTINUE
	}

	/**
	 * A class to represent predefined error conditions.
	 */
	public static class Condition {

		public static final Condition interna_server_error = new Condition(
				"internal-server-error");
		public static final Condition forbidden = new Condition("forbidden");
		public static final Condition bad_request = new Condition("bad-request");
		public static final Condition conflict = new Condition("conflict");
		public static final Condition feature_not_implemented = new Condition(
				"feature-not-implemented");
		public static final Condition gone = new Condition("gone");
		public static final Condition item_not_found = new Condition(
				"item-not-found");
		public static final Condition jid_malformed = new Condition(
				"jid-malformed");
		public static final Condition no_acceptable = new Condition(
				"not-acceptable");
		public static final Condition not_allowed = new Condition("not-allowed");
		public static final Condition not_authorized = new Condition(
				"not-authorized");
		public static final Condition payment_required = new Condition(
				"payment-required");
		public static final Condition recipient_unavailable = new Condition(
				"recipient-unavailable");
		public static final Condition redirect = new Condition("redirect");
		public static final Condition registration_required = new Condition(
				"registration-required");
		public static final Condition remote_server_error = new Condition(
				"remote-server-error");
		public static final Condition remote_server_not_found = new Condition(
				"remote-server-not-found");
		public static final Condition remote_server_timeout = new Condition(
				"remote-server-timeout");
		public static final Condition resource_constraint = new Condition(
				"resource-constraint");
		public static final Condition service_unavailable = new Condition(
				"service-unavailable");
		public static final Condition subscription_required = new Condition(
				"subscription-required");
		public static final Condition undefined_condition = new Condition(
				"undefined-condition");
		public static final Condition unexpected_request = new Condition(
				"unexpected-request");
		public static final Condition request_timeout = new Condition(
				"request-timeout");

		private String value;

		public Condition(String value) {
			this.value = value;
		}

		public String toString() {
			return value;
		}
	}

	/**
	 * A class to represent the error specification used to infer common usage.
	 */
	private static class ErrorSpecification {
		private int code;
		private Type type;
		private Condition condition;
		private static Map<Condition, ErrorSpecification> instances = errorSpecifications();

		private ErrorSpecification(Condition condition, Type type, int code) {
			this.code = code;
			this.type = type;
			this.condition = condition;
		}

		private static Map<Condition, ErrorSpecification> errorSpecifications() {
			Map<Condition, ErrorSpecification> instances = new HashMap<Condition, ErrorSpecification>(
					22);
			instances.put(Condition.interna_server_error,
					new ErrorSpecification(Condition.interna_server_error,
							Type.WAIT, 500));
			instances.put(Condition.forbidden, new ErrorSpecification(
					Condition.forbidden, Type.AUTH, 403));
			instances.put(Condition.bad_request,
					new XMPPError.ErrorSpecification(Condition.bad_request,
							Type.MODIFY, 400));
			instances.put(Condition.item_not_found,
					new XMPPError.ErrorSpecification(Condition.item_not_found,
							Type.CANCEL, 404));
			instances.put(Condition.conflict, new XMPPError.ErrorSpecification(
					Condition.conflict, Type.CANCEL, 409));
			instances
					.put(Condition.feature_not_implemented,
							new XMPPError.ErrorSpecification(
									Condition.feature_not_implemented,
									Type.CANCEL, 501));
			instances.put(Condition.gone, new XMPPError.ErrorSpecification(
					Condition.gone, Type.MODIFY, 302));
			instances.put(Condition.jid_malformed,
					new XMPPError.ErrorSpecification(Condition.jid_malformed,
							Type.MODIFY, 400));
			instances.put(Condition.no_acceptable,
					new XMPPError.ErrorSpecification(Condition.no_acceptable,
							Type.MODIFY, 406));
			instances.put(Condition.not_allowed,
					new XMPPError.ErrorSpecification(Condition.not_allowed,
							Type.CANCEL, 405));
			instances.put(Condition.not_authorized,
					new XMPPError.ErrorSpecification(Condition.not_authorized,
							Type.AUTH, 401));
			instances.put(Condition.payment_required,
					new XMPPError.ErrorSpecification(
							Condition.payment_required, Type.AUTH, 402));
			instances.put(Condition.recipient_unavailable,
					new XMPPError.ErrorSpecification(
							Condition.recipient_unavailable, Type.WAIT, 404));
			instances.put(Condition.redirect, new XMPPError.ErrorSpecification(
					Condition.redirect, Type.MODIFY, 302));
			instances.put(Condition.registration_required,
					new XMPPError.ErrorSpecification(
							Condition.registration_required, Type.AUTH, 407));
			instances
					.put(Condition.remote_server_not_found,
							new XMPPError.ErrorSpecification(
									Condition.remote_server_not_found,
									Type.CANCEL, 404));
			instances.put(Condition.remote_server_timeout,
					new XMPPError.ErrorSpecification(
							Condition.remote_server_timeout, Type.WAIT, 504));
			instances.put(Condition.remote_server_error,
					new XMPPError.ErrorSpecification(
							Condition.remote_server_error, Type.CANCEL, 502));
			instances.put(Condition.resource_constraint,
					new XMPPError.ErrorSpecification(
							Condition.resource_constraint, Type.WAIT, 500));
			instances.put(Condition.service_unavailable,
					new XMPPError.ErrorSpecification(
							Condition.service_unavailable, Type.CANCEL, 503));
			instances.put(Condition.subscription_required,
					new XMPPError.ErrorSpecification(
							Condition.subscription_required, Type.AUTH, 407));
			instances.put(Condition.undefined_condition,
					new XMPPError.ErrorSpecification(
							Condition.undefined_condition, Type.WAIT, 500));
			instances.put(Condition.unexpected_request,
					new XMPPError.ErrorSpecification(
							Condition.unexpected_request, Type.WAIT, 400));
			instances.put(Condition.request_timeout,
					new XMPPError.ErrorSpecification(Condition.request_timeout,
							Type.CANCEL, 408));

			return instances;
		}

		protected static ErrorSpecification specFor(Condition condition) {
			return instances.get(condition);
		}

		/**
		 * Returns the error condition.
		 * 
		 * @return the error condition.
		 */
		protected Condition getCondition() {
			return condition;
		}

		/**
		 * Returns the error type.
		 * 
		 * @return the error type.
		 */
		protected Type getType() {
			return type;
		}

		/**
		 * Returns the error code.
		 * 
		 * @return the error code.
		 */
		protected int getCode() {
			return code;
		}
	}
}
